<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <title>定时任务 - 为企业级框架和应用而生</title>
  <meta charset="utf-8">
  <meta name="description" content="index.description">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/images/favicon.png" type="image/x-icon">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css" />
<link rel="stylesheet" href="/css/index.css">

    <script>
    !function(t,e,a,r,c){t.TracertCmdCache=t.TracertCmdCache||[],t[c]=window[c]||
      {_isInit:!0,call:function(){t.TracertCmdCache.push(arguments)},
      start:function(t){this.call('start',t)}},t[c].l=new Date;
      var n=e.createElement(a),s=e.getElementsByTagName(a)[0];
      n.async=!0,n.src=r,s.parentNode.insertBefore(n,s)}
    (window,document,'script','https://tracert.alipay.com/tracert.js','Tracert');
      Tracert.start({
        plugins: [ 'BucName' ],
        spmAPos: 'a454',
        spmBPos: 'b4893',
      });
    </script>
  
</head>
<body>
  <div class="nav" >
  <header>
    <a href="/zh-cn/" class="nav-logo leftpadding" alt="egg"><img src="https://zos.alipayobjects.com/rmsportal/VTcUYAaoKqXyHJbLAPyF.svg"></a>
    <ul class="nav-item">
      <li>
        <form id="search-form">
          <input type="text" id="search-query" class="search-query st-default-search-input">
        </form>
      </li>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/basics/schedule.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/basics/schedule.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
      <li><iframe src="https://ghbtns.com/github-btn.html?user=eggjs&repo=egg&type=star&count=true" frameborder="0" scrolling="0" width="150px" height="20px"></iframe></li>
    </ul>
    <a id="mobileTrigger" href="#" class="mobile-trigger">
      <ul>
        <li></li>
        <li></li>
        <li></li>
      </ul>
    </a>
  </header>
</div>
  <div id="container" class="container">
    <div class="page-main">
  <article class="markdown-body">
    <h1>定时任务</h1>
    <p>虽然我们通过框架开发的 HTTP Server 是请求响应模型的，但是仍然还会有许多场景需要执行一些定时任务，例如：</p>
<ol>
<li>定时上报应用状态。</li>
<li>定时从远程接口更新本地缓存。</li>
<li>定时进行文件切割、临时文件删除。</li>
</ol>
<p>框架提供了一套机制来让定时任务的编写和维护更加优雅。</p>
<h2 id="编写定时任务"><a class="markdown-anchor" href="#编写定时任务">#</a> 编写定时任务</h2>
<p>所有的定时任务都统一存放在 <code>app/schedule</code> 目录下，每一个文件都是一个独立的定时任务，可以配置定时任务的属性和要执行的方法。</p>
<p>一个简单的例子，我们定义一个更新远程数据到内存缓存的定时任务，就可以在 <code>app/schedule</code> 目录下创建一个 <code>update_cache.js</code> 文件</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> Subscription = <span class="built_in">require</span>(<span class="string">'egg'</span>).Subscription;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UpdateCache</span> <span class="keyword">extends</span> <span class="title">Subscription</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 通过 schedule 属性来设置定时任务的执行间隔等配置</span></span><br><span class="line">  <span class="keyword">static</span> get schedule() &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      interval: <span class="string">'1m'</span>, <span class="comment">// 1 分钟间隔</span></span><br><span class="line">      type: <span class="string">'all'</span>, <span class="comment">// 指定所有的 worker 都需要执行</span></span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// subscribe 是真正定时任务执行时被运行的函数</span></span><br><span class="line">  <span class="keyword">async</span> subscribe() &#123;</span><br><span class="line">    <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="keyword">this</span>.ctx.curl(<span class="string">'http://www.api.com/cache'</span>, &#123;</span><br><span class="line">      dataType: <span class="string">'json'</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">this</span>.ctx.app.cache = res.data;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = UpdateCache;</span><br></pre></td></tr></table></figure>
<p>还可以简写为</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  schedule: &#123;</span><br><span class="line">    interval: <span class="string">'1m'</span>, <span class="comment">// 1 分钟间隔</span></span><br><span class="line">    type: <span class="string">'all'</span>, <span class="comment">// 指定所有的 worker 都需要执行</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="keyword">async</span> task(ctx) &#123;</span><br><span class="line">    <span class="keyword">const</span> res = <span class="keyword">await</span> ctx.curl(<span class="string">'http://www.api.com/cache'</span>, &#123;</span><br><span class="line">      dataType: <span class="string">'json'</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    ctx.app.cache = res.data;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>这个定时任务会在每一个 Worker 进程上每 1 分钟执行一次，将远程数据请求回来挂载到 <code>app.cache</code> 上。</p>
<h3 id="任务"><a class="markdown-anchor" href="#任务">#</a> 任务</h3>
<ul>
<li><code>task</code> 或 <code>subscribe</code> 同时支持 <code>generator function</code> 和 <code>async function</code>。</li>
<li><code>task</code> 的入参为 <code>ctx</code>，匿名的 Context 实例，可以通过它调用 <code>service</code> 等。</li>
</ul>
<h3 id="定时方式"><a class="markdown-anchor" href="#定时方式">#</a> 定时方式</h3>
<p>定时任务可以指定 interval 或者 cron 两种不同的定时方式。</p>
<h4 id="interval"><a class="markdown-anchor" href="#interval">#</a> interval</h4>
<p>通过 <code>schedule.interval</code> 参数来配置定时任务的执行时机，定时任务将会每间隔指定的时间执行一次。interval 可以配置成</p>
<ul>
<li>数字类型，单位为毫秒数，例如 <code>5000</code>。</li>
<li>字符类型，会通过 <a href="https://github.com/zeit/ms" target="_blank" rel="noopener">ms</a> 转换成毫秒数，例如 <code>5s</code>。</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  schedule: &#123;</span><br><span class="line">    <span class="comment">// 每 10 秒执行一次</span></span><br><span class="line">    interval: <span class="string">'10s'</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h4 id="cron"><a class="markdown-anchor" href="#cron">#</a> cron</h4>
<p>通过 <code>schedule.cron</code> 参数来配置定时任务的执行时机，定时任务将会按照 cron 表达式在特定的时间点执行。cron 表达式通过 <a href="https://github.com/harrisiirak/cron-parser" target="_blank" rel="noopener">cron-parser</a> 进行解析。</p>
<p><strong>注意：cron-parser 支持可选的秒（linux crontab 不支持）。</strong></p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">*    *    *    *    *    *</span><br><span class="line">┬    ┬    ┬    ┬    ┬    ┬</span><br><span class="line">│    │    │    │    │    |</span><br><span class="line">│    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)</span><br><span class="line">│    │    │    │    └───── month (1 - 12)</span><br><span class="line">│    │    │    └────────── day of month (1 - 31)</span><br><span class="line">│    │    └─────────────── hour (0 - 23)</span><br><span class="line">│    └──────────────────── minute (0 - 59)</span><br><span class="line">└───────────────────────── second (0 - 59, optional)</span><br></pre></td></tr></table></figure>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  schedule: &#123;</span><br><span class="line">    <span class="comment">// 每三小时准点执行一次</span></span><br><span class="line">    cron: <span class="string">'0 0 */3 * * *'</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h3 id="类型"><a class="markdown-anchor" href="#类型">#</a> 类型</h3>
<p>框架提供的定时任务默认支持两种类型，worker 和 all。worker 和 all 都支持上面的两种定时方式，只是当到执行时机时，会执行定时任务的 worker 不同：</p>
<ul>
<li><code>worker</code> 类型：每台机器上只有一个 worker 会执行这个定时任务，每次执行定时任务的 worker 的选择是随机的。</li>
<li><code>all</code> 类型：每台机器上的每个 worker 都会执行这个定时任务。</li>
</ul>
<h3 id="其他参数"><a class="markdown-anchor" href="#其他参数">#</a> 其他参数</h3>
<p>除了刚才介绍到的几个参数之外，定时任务还支持这些参数：</p>
<ul>
<li><code>cronOptions</code>: 配置 cron 的时区等，参见 <a href="https://github.com/harrisiirak/cron-parser#options" target="_blank" rel="noopener">cron-parser</a> 文档</li>
<li><code>immediate</code>：配置了该参数为 true 时，这个定时任务会在应用启动并 ready 后立刻执行一次这个定时任务。</li>
<li><code>disable</code>：配置该参数为 true 时，这个定时任务不会被启动。</li>
</ul>
<h3 id="执行日志"><a class="markdown-anchor" href="#执行日志">#</a> 执行日志</h3>
<p>执行日志会输出到 <code>${appInfo.root}/logs/{app_name}/egg-schedule.log</code>，默认不会输出到控制台，可以通过 <code>config.customLogger.scheduleLogger</code> 来自定义。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// config/config.default.js</span></span><br><span class="line">config.customLogger = &#123;</span><br><span class="line">  scheduleLogger: &#123;</span><br><span class="line">    <span class="comment">// consoleLevel: 'NONE',</span></span><br><span class="line">    <span class="comment">// file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'),</span></span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h3 id="动态配置定时任务"><a class="markdown-anchor" href="#动态配置定时任务">#</a> 动态配置定时任务</h3>
<p>有时候我们需要判断不同的环境来配置定时任务的参数。定时任务还有支持另一种写法：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    schedule: &#123;</span><br><span class="line">      interval: <span class="string">'1m'</span>,</span><br><span class="line">      type: <span class="string">'all'</span>,</span><br><span class="line">      disable: app.config.env === <span class="string">'local'</span>, <span class="comment">// 本地开发环境不执行</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="keyword">async</span> task(ctx) &#123;</span><br><span class="line">      <span class="keyword">const</span> res = <span class="keyword">await</span> ctx.curl(<span class="string">'http://www.api.com/cache'</span>, &#123;</span><br><span class="line">        contentType: <span class="string">'json'</span>,</span><br><span class="line">      &#125;);</span><br><span class="line">      ctx.app.cache = res.data;</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h2 id="手动执行定时任务"><a class="markdown-anchor" href="#手动执行定时任务">#</a> 手动执行定时任务</h2>
<p>我们可以通过 <code>app.runSchedule(schedulePath)</code> 来运行一个定时任务。<code>app.runSchedule</code> 接受一个定时任务文件路径（<code>app/schedule</code> 目录下的相对路径或者完整的绝对路径），执行对应的定时任务，返回一个 Promise。</p>
<p>有一些场景我们可能需要手动的执行定时任务，例如</p>
<ul>
<li>通过手动执行定时任务可以更优雅的编写对定时任务的单元测试。</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> mm = <span class="built_in">require</span>(<span class="string">'egg-mock'</span>);</span><br><span class="line"><span class="keyword">const</span> assert = <span class="built_in">require</span>(<span class="string">'assert'</span>);</span><br><span class="line"></span><br><span class="line">it(<span class="string">'should schedule work fine'</span>, <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> app = mm.app();</span><br><span class="line">  <span class="keyword">await</span> app.ready();</span><br><span class="line">  <span class="keyword">await</span> app.runSchedule(<span class="string">'update_cache'</span>);</span><br><span class="line">  assert(app.cache);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<ul>
<li>应用启动时，手动执行定时任务进行系统初始化，等初始化完毕后再启动应用。参见<a href="./app-start.html">应用启动自定义</a>章节，我们可以在 <code>app.js</code> 中编写初始化逻辑。</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.beforeStart(<span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="comment">// 保证应用启动监听端口前数据已经准备好了</span></span><br><span class="line">    <span class="comment">// 后续数据的更新由定时任务自动触发</span></span><br><span class="line">    <span class="keyword">await</span> app.runSchedule(<span class="string">'update_cache'</span>);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h2 id="扩展定时任务类型"><a class="markdown-anchor" href="#扩展定时任务类型">#</a> 扩展定时任务类型</h2>
<p>默认框架提供的定时任务只支持每台机器的单个进程执行和全部进程执行，有些情况下，我们的服务并不是单机部署的，这时候可能有一个集群的某一个进程执行一个定时任务的需求。</p>
<p>框架并没有直接提供此功能，但开发者可以在上层框架自行扩展新的定时任务类型。</p>
<p>在 <code>agent.js</code> 中继承 <code>agent.ScheduleStrategy</code>，然后通过 <code>agent.schedule.use()</code> 注册即可：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">agent</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="class"><span class="keyword">class</span> <span class="title">ClusterStrategy</span> <span class="keyword">extends</span> <span class="title">agent</span>.<span class="title">ScheduleStrategy</span> </span>&#123;</span><br><span class="line">    start() &#123;</span><br><span class="line">      <span class="comment">// 订阅其他的分布式调度服务发送的消息，收到消息后让一个进程执行定时任务</span></span><br><span class="line">      <span class="comment">// 用户在定时任务的 schedule 配置中来配置分布式调度的场景（scene）</span></span><br><span class="line">      agent.mq.subscribe(schedule.scene, () =&gt; <span class="keyword">this</span>.sendOne());</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  agent.schedule.use(<span class="string">'cluster'</span>, ClusterStrategy);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p><code>ScheduleStrategy</code> 基类提供了：</p>
<ul>
<li><code>schedule</code> - 定时任务的属性，<code>disable</code>  是默认统一支持的，其他配置可以自行解析。</li>
<li><code>this.sendOne(...args)</code> - 随机通知一个 worker 执行 task，<code>args</code> 会传递给 <code>subscribe(...args)</code> 或 <code>task(ctx, ...args)</code>。</li>
<li><code>this.sendAll(...args)</code> - 通知所有的 worker 执行 task。</li>
</ul>

  </article>
  <aside id="mobileAside" class="toc">
  <div class="mobile-menu">
    <ul>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/basics/schedule.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/basics/schedule.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
    </ul>
  </div>
  <dl><dt id="title-Intro" style="cursor: pointer;" class="aside-title">新手指南<a id="collapse-icon-Intro" class="icon opend"></a></dt><dd id=panel-Intro><ul><li><a href="/zh-cn/intro/index.html" class="menu-link">Egg.js 是什么?</a></li><li><a href="/zh-cn/intro/egg-and-koa.html" class="menu-link">Egg.js 和 Koa</a></li><li><a href="/zh-cn/intro/quickstart.html" class="menu-link">快速入门</a></li><li><a href="/zh-cn/tutorials/progressive.html" class="menu-link">渐进式开发</a></li><li><a href="/zh-cn/migration.html" class="menu-link">2.x 升级指南</a></li></ul></dd><dt id="title-Basics" style="cursor: pointer;" class="aside-title">基础功能<a id="collapse-icon-Basics" class="icon opend"></a></dt><dd id=panel-Basics><ul><li><a href="/zh-cn/basics/structure.html" class="menu-link">目录结构</a></li><li><a href="/zh-cn/basics/objects.html" class="menu-link">内置对象</a></li><li><a href="/zh-cn/basics/env.html" class="menu-link">运行环境</a></li><li><a href="/zh-cn/basics/config.html" class="menu-link">配置</a></li><li><a href="/zh-cn/basics/middleware.html" class="menu-link">中间件</a></li><li><a href="/zh-cn/basics/router.html" class="menu-link">Router</a></li><li><a href="/zh-cn/basics/controller.html" class="menu-link">Controller</a></li><li><a href="/zh-cn/basics/service.html" class="menu-link">Service</a></li><li><a href="/zh-cn/basics/plugin.html" class="menu-link">插件</a></li><li><a href="/zh-cn/basics/schedule.html" class="menu-link">定时任务</a></li><li><a href="/zh-cn/basics/extend.html" class="menu-link">框架扩展</a></li><li><a href="/zh-cn/basics/app-start.html" class="menu-link">启动自定义</a></li></ul></dd><dt id="title-Core" style="cursor: pointer;" class="aside-title">核心功能<a id="collapse-icon-Core" class="icon opend"></a></dt><dd id=panel-Core><ul><li><a href="/zh-cn/core/development.html" class="menu-link">本地开发</a></li><li><a href="/zh-cn/core/unittest.html" class="menu-link">单元测试</a></li><li><a href="/zh-cn/core/deployment.html" class="menu-link">应用部署</a></li><li><a href="/zh-cn/core/logger.html" class="menu-link">日志</a></li><li><a href="/zh-cn/core/httpclient.html" class="menu-link">HttpClient</a></li><li><a href="/zh-cn/core/cookie-and-session.html" class="menu-link">Cookie and Session</a></li><li><a href="/zh-cn/core/cluster-and-ipc.html" class="menu-link">多进程模型和进程间通讯</a></li><li><a href="/zh-cn/core/view.html" class="menu-link">模板渲染</a></li><li><a href="/zh-cn/core/error-handling.html" class="menu-link">异常处理</a></li><li><a href="/zh-cn/core/security.html" class="menu-link">安全</a></li><li><a href="/zh-cn/core/i18n.html" class="menu-link">国际化</a></li></ul></dd><dt id="title-Tutorials" style="cursor: pointer;" class="aside-title">教程<a id="collapse-icon-Tutorials" class="icon opend"></a></dt><dd id=panel-Tutorials><ul><li><a href="/zh-cn/tutorials/mysql.html" class="menu-link">MySQL</a></li><li><a href="/zh-cn/tutorials/restful.html" class="menu-link">RESTful API</a></li><li><a href="/zh-cn/tutorials/passport.html" class="menu-link">Passport 鉴权</a></li><li><a href="/zh-cn/tutorials/socketio.html" class="menu-link">Socket.IO</a></li><li><a href="/zh-cn/tutorials/assets.html" class="menu-link">静态资源</a></li><li><a href="/zh-cn/tutorials/typescript.html" class="menu-link">TypeScript</a></li></ul></dd><dt id="title-Advanced" style="cursor: pointer;" class="aside-title">进阶<a id="collapse-icon-Advanced" class="icon opend"></a></dt><dd id=panel-Advanced><ul><li><a href="/zh-cn/advanced/loader.html" class="menu-link">Loader</a></li><li><a href="/zh-cn/advanced/plugin.html" class="menu-link">插件开发</a></li><li><a href="/zh-cn/advanced/framework.html" class="menu-link">框架开发</a></li><li><a href="/zh-cn/advanced/cluster-client.html" class="menu-link">多进程研发模式增强</a></li><li><a href="/zh-cn/advanced/view-plugin.html" class="menu-link">模板插件开发规范</a></li><li><a href="/zh-cn/style-guide.html" class="menu-link">代码风格指南</a></li></ul></dd><dt id="title-Community" style="cursor: pointer;" class="aside-title">社区<a id="collapse-icon-Community" class="icon opend"></a></dt><dd id=panel-Community><ul><li><a href="/zh-cn/plugins/" class="menu-link">内置插件列表</a></li><li><a href="/zh-cn/contributing.html" class="menu-link">如何贡献</a></li><li><a href="/zh-cn/resource.html" class="menu-link">资源</a></li><li><a href="/zh-cn/faq.html" class="menu-link">常见问题</a></li></ul></dd></dl>
</aside>
<script>
var mobileTrigger = document.getElementById('mobileTrigger');
var mobileAside = document.getElementById('mobileAside');

var expandMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon opend';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = '';
  }
}

var collapseMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon closed';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = 'aside-panel-hidden';
  }
}

mobileAside.onclick = function(e) {
  const targetId = e.target.id;
  if (targetId && (targetId.indexOf('title-') > -1 || targetId.indexOf('collapse-icon-') > -1)) {
    const title = targetId.replace('title-', '').replace('collapse-icon-', '');
    try { 
      // the the browser may have no localStroage or JSON.parse may throw exception.
      const menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
        
      // current menu status
      const curClosed = menuInfo[title] ? menuInfo[title].closed : false; // default false

      // change UI
      curClosed ? expandMenu(title) : collapseMenu(title);

      // save menuInfo to localStorage
      menuInfo[title] = { closed: !curClosed } // opposite
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    } catch (e) {}
  }
};

mobileTrigger.onclick = function(e) {
  e.preventDefault();
  if (mobileAside.className.indexOf('mobile-show') === -1) {
    mobileAside.className += ' mobile-show';
  } else {
    mobileAside.className = 'toc';
  }
};

(function() {
  // save data to localStorage because the page will refresh when user change the url.
  let menuInfo;
  try { 
    // the the browser may have no localStroage or JSON.parse may throw exception.
    menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
    if (!menuInfo) {
      menuInfo = {};
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    }
  } catch (e) {
    menuInfo = {}; // default {}
  }

  for (const title in menuInfo) {
    if (menuInfo[title] && menuInfo[title].closed) { // menu in closed status.
      collapseMenu(title);
    } else {
      expandMenu(title);
    }
  }

  // highlight menu
  const pathname = window.location.pathname;
  const selector = `a[href="${pathname}"].menu-link,a[href="${pathname}index.html"].menu-link`;
  const menuItem = mobileAside.querySelector(selector);
  if (menuItem) { menuItem.className += ' highlight'; }
})();
</script>

</div>

  </div>
</body>
<script src="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js"></script>
<script>
docsearch({
  apiKey: '1561de31a86f79507ea00cdb54ce647c',
  indexName: 'eggjs',
  inputSelector: '#search-query',
});
</script>
<div class="cnzz">
<script src="https://s11.cnzz.com/z_stat.php?id=1261142226&web_id=1261142226" language="JavaScript"></script>
</div>

</html>
